<!DOCTYPE html><html lang="zh-CN" data-theme="light"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"><title>Kotlin编程第一课--(基础篇)12  实战：用Kotlin实现一个网络请求框架KtHttp | 码行</title><meta name="author" content="Carl Su"><meta name="copyright" content="Carl Su"><meta name="format-detection" content="telephone=no"><meta name="theme-color" content="ffffff"><meta name="description" content="在前面几节课当中，我们一起学习了 Kotlin 的委托、泛型、注解、反射这几个高级特性。那么今天这节课，我们将会运用这些特性，来写一个 Kotlin 版本的 HTTP 网络请求框架。由于它是纯 Kotlin 开发的，我们就把它叫做是 KtHttp 吧。 事实上，在 Java 和 Kotlin 领域，有许多出色的网络请求框架，比如 OkHttp、Retrofit、Fuel。而我们今天要实现的 KtH">
<meta property="og:type" content="article">
<meta property="og:title" content="Kotlin编程第一课--(基础篇)12  实战：用Kotlin实现一个网络请求框架KtHttp">
<meta property="og:url" content="https://devsu.top/posts/e45ff726/index.html">
<meta property="og:site_name" content="码行">
<meta property="og:description" content="在前面几节课当中，我们一起学习了 Kotlin 的委托、泛型、注解、反射这几个高级特性。那么今天这节课，我们将会运用这些特性，来写一个 Kotlin 版本的 HTTP 网络请求框架。由于它是纯 Kotlin 开发的，我们就把它叫做是 KtHttp 吧。 事实上，在 Java 和 Kotlin 领域，有许多出色的网络请求框架，比如 OkHttp、Retrofit、Fuel。而我们今天要实现的 KtH">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="https://static001.geekbang.org/resource/image/98/14/98bb006b524561dae4e80a864ae2c814.jpg">
<meta property="article:published_time" content="2022-05-13T03:46:11.000Z">
<meta property="article:modified_time" content="2023-03-23T07:42:11.977Z">
<meta property="article:author" content="Carl Su">
<meta property="article:tag" content="Kotlin">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://static001.geekbang.org/resource/image/98/14/98bb006b524561dae4e80a864ae2c814.jpg"><link rel="shortcut icon" href="https://s2.loli.net/2023/03/09/whMHdYkrKoIBnSC.png"><link rel="canonical" href="https://devsu.top/posts/e45ff726/index.html"><link rel="preconnect" href="//cdn.jsdelivr.net"/><link rel="preconnect" href="//hm.baidu.com"/><link rel="preconnect" href="//busuanzi.ibruce.info"/><link rel="stylesheet" href="/css/index.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/css/all.min.css" media="print" onload="this.media='all'"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/node-snackbar/dist/snackbar.min.css" media="print" onload="this.media='all'"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fancyapps/ui/dist/fancybox.min.css" media="print" onload="this.media='all'"><script>var _hmt = _hmt || [];
(function() {
  var hm = document.createElement("script");
  hm.src = "https://hm.baidu.com/hm.js?6243425e2ecb037931274c4ca1db3132";
  var s = document.getElementsByTagName("script")[0]; 
  s.parentNode.insertBefore(hm, s);
})();
</script><script>const GLOBAL_CONFIG = { 
  root: '/',
  algolia: undefined,
  localSearch: {"path":"/search.xml","preload":true,"languages":{"hits_empty":"找不到您查询的内容：${query}"}},
  translate: {"defaultEncoding":2,"translateDelay":0,"msgToTraditionalChinese":"繁","msgToSimplifiedChinese":"简"},
  noticeOutdate: undefined,
  highlight: {"plugin":"highlighjs","highlightCopy":true,"highlightLang":true,"highlightHeightLimit":300},
  copy: {
    success: '复制成功',
    error: '复制错误',
    noSupport: '浏览器不支持'
  },
  relativeDate: {
    homepage: false,
    post: false
  },
  runtime: '天',
  date_suffix: {
    just: '刚刚',
    min: '分钟前',
    hour: '小时前',
    day: '天前',
    month: '个月前'
  },
  copyright: {"limitCount":50,"languages":{"author":"作者: Carl Su","link":"链接: ","source":"来源: 码行","info":"著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。"}},
  lightbox: 'fancybox',
  Snackbar: {"chs_to_cht":"你已切换为繁体","cht_to_chs":"你已切换为简体","day_to_night":"你已切换为深色模式","night_to_day":"你已切换为浅色模式","bgLight":"#49b1f5","bgDark":"#1f1f1f","position":"bottom-left"},
  source: {
    justifiedGallery: {
      js: 'https://cdn.jsdelivr.net/npm/flickr-justified-gallery/dist/fjGallery.min.js',
      css: 'https://cdn.jsdelivr.net/npm/flickr-justified-gallery/dist/fjGallery.min.css'
    }
  },
  isPhotoFigcaption: true,
  islazyload: true,
  isAnchor: false,
  percent: {
    toc: true,
    rightside: true,
  }
}</script><script id="config-diff">var GLOBAL_CONFIG_SITE = {
  title: 'Kotlin编程第一课--(基础篇)12  实战：用Kotlin实现一个网络请求框架KtHttp',
  isPost: true,
  isHome: false,
  isHighlightShrink: false,
  isToc: true,
  postUpdate: '2023-03-23 15:42:11'
}</script><noscript><style type="text/css">
  #nav {
    opacity: 1
  }
  .justified-gallery img {
    opacity: 1
  }

  #recent-posts time,
  #post-meta time {
    display: inline !important
  }
</style></noscript><script>(win=>{
    win.saveToLocal = {
      set: function setWithExpiry(key, value, ttl) {
        if (ttl === 0) return
        const now = new Date()
        const expiryDay = ttl * 86400000
        const item = {
          value: value,
          expiry: now.getTime() + expiryDay,
        }
        localStorage.setItem(key, JSON.stringify(item))
      },

      get: function getWithExpiry(key) {
        const itemStr = localStorage.getItem(key)

        if (!itemStr) {
          return undefined
        }
        const item = JSON.parse(itemStr)
        const now = new Date()

        if (now.getTime() > item.expiry) {
          localStorage.removeItem(key)
          return undefined
        }
        return item.value
      }
    }
  
    win.getScript = url => new Promise((resolve, reject) => {
      const script = document.createElement('script')
      script.src = url
      script.async = true
      script.onerror = reject
      script.onload = script.onreadystatechange = function() {
        const loadState = this.readyState
        if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
        script.onload = script.onreadystatechange = null
        resolve()
      }
      document.head.appendChild(script)
    })
  
    win.getCSS = (url,id = false) => new Promise((resolve, reject) => {
      const link = document.createElement('link')
      link.rel = 'stylesheet'
      link.href = url
      if (id) link.id = id
      link.onerror = reject
      link.onload = link.onreadystatechange = function() {
        const loadState = this.readyState
        if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
        link.onload = link.onreadystatechange = null
        resolve()
      }
      document.head.appendChild(link)
    })
  
      win.activateDarkMode = function () {
        document.documentElement.setAttribute('data-theme', 'dark')
        if (document.querySelector('meta[name="theme-color"]') !== null) {
          document.querySelector('meta[name="theme-color"]').setAttribute('content', '#0d0d0d')
        }
      }
      win.activateLightMode = function () {
        document.documentElement.setAttribute('data-theme', 'light')
        if (document.querySelector('meta[name="theme-color"]') !== null) {
          document.querySelector('meta[name="theme-color"]').setAttribute('content', 'ffffff')
        }
      }
      const t = saveToLocal.get('theme')
    
          const now = new Date()
          const hour = now.getHours()
          const isNight = hour <= 6 || hour >= 18
          if (t === undefined) isNight ? activateDarkMode() : activateLightMode()
          else if (t === 'light') activateLightMode()
          else activateDarkMode()
        
      const asideStatus = saveToLocal.get('aside-status')
      if (asideStatus !== undefined) {
        if (asideStatus === 'hide') {
          document.documentElement.classList.add('hide-aside')
        } else {
          document.documentElement.classList.remove('hide-aside')
        }
      }
    
    const detectApple = () => {
      if(/iPad|iPhone|iPod|Macintosh/.test(navigator.userAgent)){
        document.documentElement.classList.add('apple')
      }
    }
    detectApple()
    })(window)</script><meta name="generator" content="Hexo 6.3.0"></head><body><div id="loading-box"><div class="loading-left-bg"></div><div class="loading-right-bg"></div><div class="spinner-box"><div class="configure-border-1"><div class="configure-core"></div></div><div class="configure-border-2"><div class="configure-core"></div></div><div class="loading-word">加载中...</div></div></div><script>const preloader = {
  endLoading: () => {
    document.body.style.overflow = 'auto';
    document.getElementById('loading-box').classList.add("loaded")
  },
  initLoading: () => {
    document.body.style.overflow = '';
    document.getElementById('loading-box').classList.remove("loaded")

  }
}
window.addEventListener('load',()=> { preloader.endLoading() })

if (false) {
  document.addEventListener('pjax:send', () => { preloader.initLoading() })
  document.addEventListener('pjax:complete', () => { preloader.endLoading() })
}</script><div id="sidebar"><div id="menu-mask"></div><div id="sidebar-menus"><div class="avatar-img is-center"><img src="https://s2.loli.net/2023/03/09/QrI9mgJ4XlbEPzN.png" onerror="onerror=null;src='/img/friend_404.gif'" alt="avatar"/></div><div class="sidebar-site-data site-data is-center"><a href="/archives/"><div class="headline">文章</div><div class="length-num">169</div></a><a href="/tags/"><div class="headline">标签</div><div class="length-num">37</div></a><a href="/categories/"><div class="headline">分类</div><div class="length-num">38</div></a></div><hr/><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> 首页</span></a></div><div class="menus_item"><a class="site-page" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> 时间轴</span></a></div><div class="menus_item"><a class="site-page" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> 归档</span></a></div><div class="menus_item"><a class="site-page" href="/tags/"><i class="fa-fw fas fa-tags"></i><span> 标签</span></a></div><div class="menus_item"><a class="site-page" href="/categories/"><i class="fa-fw fas fa-folder-open"></i><span> 分类</span></a></div><div class="menus_item"><a class="site-page group" href="javascript:void(0);"><i class="fa-fw fas fa-list"></i><span> 清单</span><i class="fas fa-chevron-down"></i></a><ul class="menus_item_child"><li><a class="site-page child" href="/music/"><i class="fa-fw fas fa-music"></i><span> 音乐</span></a></li><li><a class="site-page child" href="/Gallery/"><i class="fa-fw fas fa-images"></i><span> 照片</span></a></li><li><a class="site-page child" href="/movies/"><i class="fa-fw fas fa-video"></i><span> 电影</span></a></li></ul></div><div class="menus_item"><a class="site-page" href="/link/"><i class="fa-fw fas fa-link"></i><span> 友链</span></a></div><div class="menus_item"><a class="site-page" href="/about/"><i class="fa-fw fas fa-heart"></i><span> 关于</span></a></div></div></div></div><div class="post" id="body-wrap"><header class="post-bg fixed" id="page-header" style="background-image: url('https://static001.geekbang.org/resource/image/98/14/98bb006b524561dae4e80a864ae2c814.jpg')"><nav id="nav"><span id="blog-info"><a href="/" title="码行"><img class="site-icon" src="https://s2.loli.net/2023/03/09/whMHdYkrKoIBnSC.png"/><span class="site-name">码行</span></a></span><div id="menus"><div id="search-button"><a class="site-page social-icon search" href="javascript:void(0);"><i class="fas fa-search fa-fw"></i><span> 搜索</span></a></div><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> 首页</span></a></div><div class="menus_item"><a class="site-page" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> 时间轴</span></a></div><div class="menus_item"><a class="site-page" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> 归档</span></a></div><div class="menus_item"><a class="site-page" href="/tags/"><i class="fa-fw fas fa-tags"></i><span> 标签</span></a></div><div class="menus_item"><a class="site-page" href="/categories/"><i class="fa-fw fas fa-folder-open"></i><span> 分类</span></a></div><div class="menus_item"><a class="site-page group" href="javascript:void(0);"><i class="fa-fw fas fa-list"></i><span> 清单</span><i class="fas fa-chevron-down"></i></a><ul class="menus_item_child"><li><a class="site-page child" href="/music/"><i class="fa-fw fas fa-music"></i><span> 音乐</span></a></li><li><a class="site-page child" href="/Gallery/"><i class="fa-fw fas fa-images"></i><span> 照片</span></a></li><li><a class="site-page child" href="/movies/"><i class="fa-fw fas fa-video"></i><span> 电影</span></a></li></ul></div><div class="menus_item"><a class="site-page" href="/link/"><i class="fa-fw fas fa-link"></i><span> 友链</span></a></div><div class="menus_item"><a class="site-page" href="/about/"><i class="fa-fw fas fa-heart"></i><span> 关于</span></a></div></div><div id="toggle-menu"><a class="site-page" href="javascript:void(0);"><i class="fas fa-bars fa-fw"></i></a></div></div></nav><div id="post-info"><h1 class="post-title">Kotlin编程第一课--(基础篇)12  实战：用Kotlin实现一个网络请求框架KtHttp</h1><div id="post-meta"><div class="meta-firstline"><span class="post-meta-date"><i class="far fa-calendar-alt fa-fw post-meta-icon"></i><span class="post-meta-label">发表于</span><time class="post-meta-date-created" datetime="2022-05-13T03:46:11.000Z" title="发表于 2022-05-13 11:46:11">2022-05-13</time><span class="post-meta-separator">|</span><i class="fas fa-history fa-fw post-meta-icon"></i><span class="post-meta-label">更新于</span><time class="post-meta-date-updated" datetime="2023-03-23T07:42:11.977Z" title="更新于 2023-03-23 15:42:11">2023-03-23</time></span><span class="post-meta-categories"><span class="post-meta-separator">|</span><i class="fas fa-inbox fa-fw post-meta-icon"></i><a class="post-meta-categories" href="/categories/Android/">Android</a><i class="fas fa-angle-right post-meta-separator"></i><i class="fas fa-inbox fa-fw post-meta-icon"></i><a class="post-meta-categories" href="/categories/Android/Kotlin/">Kotlin</a><i class="fas fa-angle-right post-meta-separator"></i><i class="fas fa-inbox fa-fw post-meta-icon"></i><a class="post-meta-categories" href="/categories/Android/Kotlin/Kotlin%E7%BC%96%E7%A8%8B%E7%AC%AC%E4%B8%80%E8%AF%BE/">Kotlin编程第一课</a></span></div><div class="meta-secondline"><span class="post-meta-separator">|</span><span class="post-meta-wordcount"><i class="far fa-file-word fa-fw post-meta-icon"></i><span class="post-meta-label">字数总计:</span><span class="word-count">6.3k</span><span class="post-meta-separator">|</span><i class="far fa-clock fa-fw post-meta-icon"></i><span class="post-meta-label">阅读时长:</span><span>23分钟</span></span><span class="post-meta-separator">|</span><span class="post-meta-pv-cv" id="" data-flag-title="Kotlin编程第一课--(基础篇)12  实战：用Kotlin实现一个网络请求框架KtHttp"><i class="far fa-eye fa-fw post-meta-icon"></i><span class="post-meta-label">阅读量:</span><span id="busuanzi_value_page_pv"><i class="fa-solid fa-spinner fa-spin"></i></span></span></div></div></div></header><main class="layout" id="content-inner"><div id="post"><article class="post-content" id="article-container"><p>在前面几节课当中，我们一起学习了 Kotlin 的委托、泛型、注解、反射这几个高级特性。那么今天这节课，我们将会运用这些特性，来写一个 <strong>Kotlin 版本的 HTTP 网络请求框架</strong>。由于它是纯 Kotlin 开发的，我们就把它叫做是 KtHttp 吧。</p>
<p>事实上，在 Java 和 Kotlin 领域，有许多出色的网络请求框架，比如 <a target="_blank" rel="noopener" href="https://github.com/square/okhttp">OkHttp</a>、<a target="_blank" rel="noopener" href="https://github.com/square/Retrofit">Retrofit</a>、<a target="_blank" rel="noopener" href="https://github.com/kittinunf/fuel">Fuel</a>。而我们今天要实现的 KtHttp，它的灵感来自于 Retrofit。之所以选择 Retrofit 作为借鉴的对象，是因为它的底层使用了大量的泛型、注解和反射的技术。如果你能跟着我一起用泛型、注解、反射来实现一个简单的网络请求框架，相信你对这几个知识点的认识也会更加透彻。</p>
<p>在这节课当中，我会带你从 0 开始实现这个网络请求框架。和往常一样，为了方便你理解，我们的代码会分为两个版本：</p>
<ul>
<li>1.0 版本，我们会用 Java 思维，以最简单直白的方式来实现 KtHttp 的基础功能——同步式的 GET 网络请求；</li>
<li>2.0 版本，我们会用函数式思维来重构代码。</li>
</ul>
<p>另外，在正式开始学习之前，我也建议你去 clone 我 GitHub 上面的 KtHttp 工程：<a target="_blank" rel="noopener" href="https://github.com/chaxiu/KtHttp.git%EF%BC%8C%E7%84%B6%E5%90%8E%E7%94%A8">https://github.com/chaxiu/KtHttp.git，然后用</a> IntelliJ 打开，并切换到 start 分支跟着课程一步步敲代码。</p>
<h2 id="1-0：Java-思维"><a href="#1-0：Java-思维" class="headerlink" title="1.0：Java 思维"></a>1.0：Java 思维</h2><p>在正式开始之前，我们还是先来看看程序的运行效果：</p>
<img src= "https://s2.loli.net/2023/03/09/dog2qQY83iTIvHM.gif" data-lazy-src="https://static001.geekbang.org/resource/image/32/bc/321d151db077766997ed8d8b911f1fbc.gif?wh=1294x862" alt="img" style="zoom:50%;" />

<p>在上面的动图中，我们通过 KtHttp 请求了一个服务器的 API，然后在控制台输出了结果。这其实是我们在开发工作当中十分常见的需求。通过这个 KtHttp，我们就可以在程序当中访问任何服务器的 API，比如<a target="_blank" rel="noopener" href="https://docs.github.com/en">GitHub 的 API</a>。</p>
<p>那么，为了描述服务器返回的内容，我们定义了两个数据类：</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">// 这种写法是有问题的，但这节课我们先不管。</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">data</span> <span class="keyword">class</span> <span class="title class_">RepoList</span>(</span><br><span class="line">    <span class="keyword">var</span> count: <span class="built_in">Int</span>?,</span><br><span class="line">    <span class="keyword">var</span> items: List&lt;Repo&gt;?,</span><br><span class="line">    <span class="keyword">var</span> msg: String?</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">data</span> <span class="keyword">class</span> <span class="title class_">Repo</span>(</span><br><span class="line">    <span class="keyword">var</span> added_stars: String?,</span><br><span class="line">    <span class="keyword">var</span> avatars: List&lt;String&gt;?,</span><br><span class="line">    <span class="keyword">var</span> desc: String?,</span><br><span class="line">    <span class="keyword">var</span> forks: String?,</span><br><span class="line">    <span class="keyword">var</span> lang: String?,</span><br><span class="line">    <span class="keyword">var</span> repo: String?,</span><br><span class="line">    <span class="keyword">var</span> repo_link: String?,</span><br><span class="line">    <span class="keyword">var</span> stars: String?</span><br><span class="line">)</span><br></pre></td></tr></table></figure>

<p>除了数据类以外，我们还要定义一个用于网络请求的接口：</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">ApiService</span> &#123;</span><br><span class="line">    <span class="meta">@GET(<span class="string">&quot;/repo&quot;</span>)</span></span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="title">repos</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="meta">@Field(<span class="string">&quot;lang&quot;</span>)</span> lang: <span class="type">String</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="meta">@Field(<span class="string">&quot;since&quot;</span>)</span> since: <span class="type">String</span></span></span></span><br><span class="line"><span class="params"><span class="function">    )</span></span>: RepoList</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在这个接口当中，有两个注解，我们一个个分析：</p>
<ul>
<li><strong>GET 注解</strong>，代表了这个网络请求应该是 GET 请求，这是HTTP请求的一种方式。GET 注解当中的“&#x2F;repo”，代表了 API 的 path，它是和 baseURL 拼接的；</li>
<li><strong>Field 注解</strong>，代表了 GET 请求的参数。Field 注解当中的值也会和 URL 拼接在一起。</li>
</ul>
<p>也许你会好奇，<strong>GET、Field 这两个注解是从哪里来的呢？</strong>这其实也是需要我们自己定义的。根据上节课学过的内容，我们很容易就能写出下面的代码：</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">@Target(AnnotationTarget.FUNCTION)</span></span><br><span class="line"><span class="meta">@Retention(AnnotationRetention.RUNTIME)</span></span><br><span class="line"><span class="keyword">annotation</span> <span class="keyword">class</span> <span class="title class_">GET</span>(<span class="keyword">val</span> value: String)</span><br><span class="line"></span><br><span class="line"><span class="meta">@Target(AnnotationTarget.VALUE_PARAMETER)</span></span><br><span class="line"><span class="meta">@Retention(AnnotationRetention.RUNTIME)</span></span><br><span class="line"><span class="keyword">annotation</span> <span class="keyword">class</span> <span class="title class_">Field</span>(<span class="keyword">val</span> value: String)</span><br></pre></td></tr></table></figure>

<p>从这段代码里我们可以看出，GET 注解只能用于修饰函数，Field 注解只能用于修饰参数。另外，这两个注解的 Retention 都是 AnnotationRetention.RUNTIME，这意味着这两个注解都是运行时可访问的。而这，也正好是我们后面要使用的反射的前提。</p>
<p>最后，我们再来看看 KtHttp 是如何使用的：</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="comment">// ①</span></span><br><span class="line">    <span class="keyword">val</span> api: ApiService = KtHttpV1.create(ApiService::<span class="keyword">class</span>.java)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ②</span></span><br><span class="line">    <span class="keyword">val</span> <span class="keyword">data</span>: RepoList = api.repos(lang = <span class="string">&quot;Kotlin&quot;</span>, since = <span class="string">&quot;weekly&quot;</span>)</span><br><span class="line"></span><br><span class="line">    println(<span class="keyword">data</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>上面的代码有两个注释，我们分别来看。</p>
<ul>
<li>注释①：我们调用 KtHttpV1.create() 方法，传入了 ApiService::class.java，参数的类型是Class，返回值类型是 ApiService。这就相当于创建了 ApiService 这个接口的实现类的对象。</li>
<li>注释②：我们调用 api.repos() 这个方法，传入了 Kotlin、weekly 这两个参数，代表我们想查询最近一周最热门的 Kotlin 开源项目。</li>
</ul>
<p>看到这里，你也许会好奇，KtHttpV1.create() 是如何创建 ApiService 的实例的呢？要知道 ApiService 可是一个接口，我们要创建它的对象，必须要先定义一个类实现它的接口方法，然后再用这个类来创建对象才行。</p>
<p>不过在这里，我们不会使用这种传统的方式，而是会用动态代理，也就是 JDK 的<a target="_blank" rel="noopener" href="https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/Proxy.html">Proxy</a>。Proxy 的底层，其实也用到了反射。</p>
<p>不过，由于这个案例涉及到的知识点都很抽象，在正式开始编写逻辑代码之前，我们先来看看下面这个动图，对整体的程序有一个粗略的认识。</p>
<img src= "https://s2.loli.net/2023/03/09/dog2qQY83iTIvHM.gif" data-lazy-src="https://static001.geekbang.org/resource/image/cf/95/cf441d3df1bb4b49432319e160cd3c95.gif?wh=1080x608" alt="img" style="zoom:67%;" />

<p>现在，相信你大概就知道这个程序是如何实现的了。下面，我再带你来看看具体的代码是怎么写的。</p>
<p>这里我要先说明一点，为了不偏离这次实战课的主题，我们不会去深究 Proxy 的底层原理。在这里，<strong>你只需要知道，我们通过 Proxy，就可以动态地创建 ApiService 接口的实例化对象</strong>。具体的做法如下：</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="type">&lt;T&gt;</span> <span class="title">create</span><span class="params">(service: <span class="type">Class</span>&lt;<span class="type">T</span>&gt;)</span></span>: T &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 调用 Proxy.newProxyInstance 就可以创建接口的实例化对象</span></span><br><span class="line">    <span class="keyword">return</span> Proxy.newProxyInstance(</span><br><span class="line">        service.classLoader,</span><br><span class="line">        arrayOf&lt;Class&lt;*&gt;&gt;(service),</span><br><span class="line">        <span class="keyword">object</span> : InvocationHandler&#123;</span><br><span class="line">            <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">invoke</span><span class="params">(proxy: <span class="type">Any</span>?, method: <span class="type">Method</span>?, args: <span class="type">Array</span>&lt;<span class="type">out</span> <span class="type">Any</span>&gt;?)</span></span>: Any &#123;</span><br><span class="line">                <span class="comment">// 省略</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    ) <span class="keyword">as</span> T</span><br><span class="line">&#125;    </span><br></pre></td></tr></table></figure>

<p>在上面的代码当中，我们在 create() 方法当中，直接返回了 Proxy.newProxyInstance() 这个方法的返回值，最后再将其转换成了 T 类型。</p>
<p>那么，newProxyInstance() 这个方法又是如何定义的呢？</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">public</span> static Object newProxyInstance(ClassLoader loader,</span><br><span class="line">                                          Class&lt;?&gt;[] interfaces,</span><br><span class="line">                                          InvocationHandler h)&#123; </span><br><span class="line">        ...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">InvocationHandler</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> Object invoke(Object proxy, Method method, Object[] args)</span><br><span class="line">        throws Throwable;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>从上面的代码当中，我们可以看到，最后一个参数，InvocationHandler 其实是符合 SAM 转换要求的，所以我们的 create() 方法可以进一步简化成这样：</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="type">&lt;T&gt;</span> <span class="title">create</span><span class="params">(service: <span class="type">Class</span>&lt;<span class="type">T</span>&gt;)</span></span>: T &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> Proxy.newProxyInstance(</span><br><span class="line">        service.classLoader,</span><br><span class="line">        arrayOf&lt;Class&lt;*&gt;&gt;(service)</span><br><span class="line">    ) &#123; proxy, method, args -&gt;</span><br><span class="line">        <span class="comment">// 待完成</span></span><br><span class="line">    &#125; <span class="keyword">as</span> T</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>那么到这里，我们程序的基本框架也就搭建好了。</p>
<p>细心的你一定发现了，我们<strong>程序的主要逻辑还没实现</strong>，所以接下来，我们就一起看看上面那个“待完成”的 InvocationHandler，这个 Lambda 表达式应该怎么写。这个换句话说，也就是 Proxy.newProxyInstance()，会帮我们创建 ApiService 的实例对象，而 ApiService 当中的接口方法的具体逻辑，我们需要在 Lambda 表达式当中实现。</p>
<p>好了，让我们回过头来看看 ApiService 当中的代码细节：</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">ApiService</span> &#123;</span><br><span class="line"><span class="comment">// 假设我们的baseurl是：https://baseurl.com</span></span><br><span class="line"><span class="comment">// 这里拼接结果会是这样：https://baseurl.com/repo</span></span><br><span class="line"><span class="comment">//          ↓</span></span><br><span class="line">    <span class="meta">@GET(<span class="string">&quot;/repo&quot;</span>)</span></span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="title">repos</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">    <span class="comment">//                Field注解当中的lang，最终会拼接到url当中去</span></span></span></span><br><span class="line"><span class="params"><span class="function">    <span class="comment">//            ↓                                                 ↓</span></span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="meta">@Field(<span class="string">&quot;lang&quot;</span>)</span> lang: <span class="type">String</span>,  <span class="comment">// https://baseurl.com/repo?lang=Kotlin</span></span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="meta">@Field(<span class="string">&quot;since&quot;</span>)</span> since: <span class="type">String</span> <span class="comment">// https://baseurl.com/repo?lang=Kotlin&amp;since=weekly</span></span></span></span><br><span class="line"><span class="params"><span class="function">    )</span></span>: RepoList</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>从代码注释中可以看出来，其实我们真正需要实现的逻辑，就是想办法把注解当中的值 &#x2F;repo、lang、since 取出来，然后拼接到 URL 当中去。那么，我们如何才能得到注解当中的值呢？</p>
<p>答案自然就是我们在上节课学过的：<strong>反射</strong>。</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">object</span> KtHttpV1 &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 底层使用 OkHttp</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">var</span> okHttpClient: OkHttpClient = OkHttpClient()</span><br><span class="line">    <span class="comment">// 使用 Gson 解析 JSON</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">var</span> gson: Gson = Gson()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 这里以baseurl.com为例，实际上我们的KtHttpV1可以请求任意API</span></span><br><span class="line">    <span class="keyword">var</span> baseUrl = <span class="string">&quot;https://baseurl.com&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="type">&lt;T&gt;</span> <span class="title">create</span><span class="params">(service: <span class="type">Class</span>&lt;<span class="type">T</span>&gt;)</span></span>: T &#123;</span><br><span class="line">        <span class="keyword">return</span> Proxy.newProxyInstance(</span><br><span class="line">            service.classLoader,</span><br><span class="line">            arrayOf&lt;Class&lt;*&gt;&gt;(service)</span><br><span class="line">        <span class="comment">//           ①     ②</span></span><br><span class="line">        <span class="comment">//           ↓      ↓</span></span><br><span class="line">        ) &#123; proxy, method, args -&gt;</span><br><span class="line">            <span class="comment">// ③</span></span><br><span class="line">            <span class="keyword">val</span> annotations = method.annotations</span><br><span class="line">            <span class="keyword">for</span> (<span class="keyword">annotation</span> <span class="keyword">in</span> annotations) &#123;</span><br><span class="line">                <span class="comment">// ④</span></span><br><span class="line">                <span class="keyword">if</span> (<span class="keyword">annotation</span> <span class="keyword">is</span> GET) &#123;</span><br><span class="line">                    <span class="comment">// ⑤</span></span><br><span class="line">                    <span class="keyword">val</span> url = baseUrl + <span class="keyword">annotation</span>.value</span><br><span class="line">                    <span class="comment">// ⑥</span></span><br><span class="line">                    <span class="keyword">return</span><span class="symbol">@newProxyInstance</span> invoke(url, method, args!!)</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span><span class="symbol">@newProxyInstance</span> <span class="literal">null</span></span><br><span class="line"></span><br><span class="line">        &#125; <span class="keyword">as</span> T</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">invoke</span><span class="params">(url: <span class="type">String</span>, method: <span class="type">Method</span>, args: <span class="type">Array</span>&lt;<span class="type">Any</span>&gt;)</span></span>: Any? &#123;</span><br><span class="line">        <span class="comment">// 待完成</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在上面的代码中，一共有 6 个注释，我们一个个看。</p>
<ul>
<li>注释①：method 的类型是反射后的 Method，在我们这个例子当中，它最终会代表被调用的方法，也就是 ApiService 接口里面的 repos() 这个方法。</li>
<li>注释②：args 的类型是对象的数组，在我们的例子当中，它最终会代表方法的参数的值，也就是“api.repos(“Kotlin”, “weekly”)”当中的”Kotlin”和”weekly”。</li>
<li>注释③：method.annotations，代表了我们会取出 repos() 这个方法上面的所有注解，由于 repos() 这个方法上面可能会有多个注解，因此它是数组类型。</li>
<li>注释④：我们使用 for 循环，遍历所有的注解，找到 GET 注解。</li>
<li>注释⑤：我们找到 GET 注解以后，要取出 @GET(“&#x2F;repo”) 当中的”&#x2F;repo”，也就是“annotation.value”。这时候我们只需要用它与 baseURL 进行拼接，就可以得到完整的 URL；</li>
<li>注释⑥：return@newProxyInstance，用的是 Lambda 表达式当中的返回语法，在得到完整的 URL 以后，我们将剩下的逻辑都交给了 invoke() 这个方法。</li>
</ul>
<p>接下来，我们再来看看 invoke() 当中的“待完成代码”应该怎么写。</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">invoke</span><span class="params">(url: <span class="type">String</span>, method: <span class="type">Method</span>, args: <span class="type">Array</span>&lt;<span class="type">Any</span>&gt;)</span></span>: Any? &#123;</span><br><span class="line">    <span class="comment">// ① 根据url拼接参数，也就是：url + ?lang=Kotlin&amp;since=weekly</span></span><br><span class="line">    <span class="comment">// ② 使用okHttpClient进行网络请求</span></span><br><span class="line">    <span class="comment">// ③ 使用gson进行JSON解析</span></span><br><span class="line">    <span class="comment">// ④ 返回结果</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在上面的代码中，我们的 invoke() 方法一共分成了四个步骤，其中的③、④两个步骤其实很容易实现：</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">invoke</span><span class="params">(url: <span class="type">String</span>, method: <span class="type">Method</span>, args: <span class="type">Array</span>&lt;<span class="type">Any</span>&gt;)</span></span>: Any? &#123;</span><br><span class="line">    <span class="comment">// ① 根据url拼接参数，也就是：url + ?lang=Kotlin&amp;since=weekly</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 使用okHttpClient进行网络请求</span></span><br><span class="line">    <span class="keyword">val</span> request = Request.Builder()</span><br><span class="line">            .url(url)</span><br><span class="line">            .build()</span><br><span class="line">    <span class="keyword">val</span> response = okHttpClient.newCall(request).execute()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ② 获取repos()的返回值类型 genericReturnType</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 使用gson进行JSON解析</span></span><br><span class="line">    <span class="keyword">val</span> body = response.body</span><br><span class="line">    <span class="keyword">val</span> json = body?.string()</span><br><span class="line">    <span class="comment">//                              根据repos()的返回值类型解析JSON</span></span><br><span class="line">    <span class="comment">//                                            ↓</span></span><br><span class="line">    <span class="keyword">val</span> result = gson.fromJson&lt;Any?&gt;(json, genericReturnType)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 返回结果</span></span><br><span class="line">    <span class="keyword">return</span> result</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>继续看，经过我们的分解，现在的问题变成了下面这样：</p>
<ul>
<li>注释①，利用反射，解析出“api.repos(“Kotlin”, “weekly”)”这个方法当中的”Kotlin”和”weekly”，将其与 URL 进行拼接得到：url + ?lang&#x3D;Kotlin&amp;since&#x3D;weekly</li>
<li>注释②，利用反射，解析出 repos() 的返回值类型，用于 JSON 解析。</li>
</ul>
<p>我们来看看最终的代码：</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">invoke</span><span class="params">(path: <span class="type">String</span>, method: <span class="type">Method</span>, args: <span class="type">Array</span>&lt;<span class="type">Any</span>&gt;)</span></span>: Any? &#123;</span><br><span class="line">    <span class="comment">// 条件判断</span></span><br><span class="line">    <span class="keyword">if</span> (method.parameterAnnotations.size != args.size) <span class="keyword">return</span> <span class="literal">null</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 解析完整的url</span></span><br><span class="line">    <span class="keyword">var</span> url = path</span><br><span class="line">    <span class="comment">// ①</span></span><br><span class="line">    <span class="keyword">val</span> parameterAnnotations = method.parameterAnnotations</span><br><span class="line">    <span class="keyword">for</span> (i <span class="keyword">in</span> parameterAnnotations.indices) &#123;</span><br><span class="line">        <span class="keyword">for</span> (parameterAnnotation <span class="keyword">in</span> parameterAnnotations[i]) &#123;</span><br><span class="line">            <span class="comment">// ②</span></span><br><span class="line">            <span class="keyword">if</span> (parameterAnnotation <span class="keyword">is</span> Field) &#123;</span><br><span class="line">                <span class="keyword">val</span> key = parameterAnnotation.value</span><br><span class="line">                <span class="keyword">val</span> value = args[i].toString()</span><br><span class="line">                <span class="keyword">if</span> (!url.contains(<span class="string">&quot;?&quot;</span>)) &#123;</span><br><span class="line">                    <span class="comment">// ③</span></span><br><span class="line">                    url += <span class="string">&quot;?<span class="variable">$key</span>=<span class="variable">$value</span>&quot;</span></span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    <span class="comment">// ④</span></span><br><span class="line">                    url += <span class="string">&quot;&amp;<span class="variable">$key</span>=<span class="variable">$value</span>&quot;</span></span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 最终的url会是这样：</span></span><br><span class="line">    <span class="comment">// https://baseurl.com/repo?lang=Kotlin&amp;since=weekly</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 执行网络请求</span></span><br><span class="line">    <span class="keyword">val</span> request = Request.Builder()</span><br><span class="line">        .url(url)</span><br><span class="line">        .build()</span><br><span class="line">    <span class="keyword">val</span> response = okHttpClient.newCall(request).execute()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ⑤</span></span><br><span class="line">    <span class="keyword">val</span> genericReturnType = method.genericReturnType</span><br><span class="line">    <span class="keyword">val</span> body = response.body</span><br><span class="line">    <span class="keyword">val</span> json = body?.string()</span><br><span class="line">    <span class="comment">// JSON解析</span></span><br><span class="line">    <span class="keyword">val</span> result = gson.fromJson&lt;Any?&gt;(json, genericReturnType)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 返回值</span></span><br><span class="line">    <span class="keyword">return</span> result</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>上面的代码一共涉及五个注释，它们都是跟注解与反射这两个知识点相关的。</p>
<ul>
<li>注释①，method.parameterAnnotations，它的作用是取出方法参数当中的所有注解，在我们这个案例当中，repos() 这个方法当中涉及到两个注解，它们分别是@Field(“lang”)、@Field(“since”)。</li>
<li>注释②，由于方法当中可能存在其他注解，因此要筛选出我们想要的 Field 注解。</li>
<li>注释③，这里是取出注解当中的值“lang”，以及参数当中对应的值“Kotlin”进行拼接，URL 第一次拼接参数的时候，要用“?”分隔。</li>
<li>注释④，这里是取出注解当中的值“since”，以及参数当中对应的值“weekly”进行拼接，后面的参数拼接格式，是用“&amp;”分隔。</li>
<li>注释⑤，method.genericReturnType 取出 repos() 的返回值类型，也就是 RepoList，最终，我们用它来解析 JSON。</li>
</ul>
<p>说实话，动态代理的这种模式，由于它大量应用了反射，加之我们的代码当中还牵涉到了泛型和注解，导致这个案例的代码不是那么容易理解。不过，我们其实可以利用<strong>调试</strong>的手段，去查看代码当中每一步执行的结果，这样就能对注解、反射、动态代理有一个更具体的认识。</p>
<p>前面带你看过的这个动图，其实就是在向你展示代码在调试过程中的关键节点，我们可以再来回顾一下整个代码的执行流程：</p>
<img src= "https://s2.loli.net/2023/03/09/dog2qQY83iTIvHM.gif" data-lazy-src="https://static001.geekbang.org/resource/image/cf/95/cf441d3df1bb4b49432319e160cd3c95.gif?wh=1080x608" alt="img" style="zoom:50%;" />

<p>相信现在，你已经能够体会我们使用 <strong>动态代理 + 注解 + 反射</strong> 实现这个网络请求框架的原因了。通过这样的方式，我们就不必在代码当中去实现每一个接口，而是只要是符合这样的代码模式，任意的接口和方法，我们都可以直接传进去。在这个例子当中，我们用的是 ApiService 这个接口，如果下次我们定义了另一个接口，比如说：</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">GitHubService</span> &#123;</span><br><span class="line">    <span class="meta">@GET(<span class="string">&quot;/search&quot;</span>)</span></span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="title">search</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="meta">@Field(<span class="string">&quot;id&quot;</span>)</span> id: <span class="type">String</span></span></span></span><br><span class="line"><span class="params"><span class="function">    )</span></span>: User</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这时候，我们的 KtHttp 根本不需要做任何的改动，直接这样调用即可：</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    KtHttpV1.baseUrl = <span class="string">&quot;https://api.github.com&quot;</span></span><br><span class="line">    <span class="comment">//       换一个接口名即可                  换一个接口名即可</span></span><br><span class="line">    <span class="comment">//              ↓                             ↓                </span></span><br><span class="line">    <span class="keyword">val</span> api: GitHubService = KtHttpV1.create(GitHubService::<span class="keyword">class</span>.java)</span><br><span class="line">    <span class="keyword">val</span> <span class="keyword">data</span>: User = api.search(id = <span class="string">&quot;JetBrains&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>可以发现，使用动态代理实现网络请求的优势，它的<strong>灵活性</strong>是非常好的。只要我们定义的 Service 接口拥有对应的注解 GET、Field，我们就可以通过注解与反射，将这些信息拼凑在一起。下面这个动图就展示了它们整体的流程：</p>
<img src= "https://s2.loli.net/2023/03/09/dog2qQY83iTIvHM.gif" data-lazy-src="https://static001.geekbang.org/resource/image/8b/31/8b5997dc2f036020dc16a1a5efb8c531.gif?wh=1080x608" alt="img" style="zoom:50%;" />

<p>实际上，我们的 KtHttp，就是将 URL 的信息存储在了注解当中（比如 lang 和 since），而实际的参数值，是在函数调用的时候传进来的（比如 Kotlin 和 weekly）。我们通过泛型、注解、反射的结合，将这些信息集到一起，完成整个 URL 的拼接，最后才通过 OkHttp 完成的网络请求、Gson 完成的解析。</p>
<p>好，到这里，我们 1.0 版本的开发就算是完成了。这里的单元测试代码很容易写，我就不贴出来了，<strong>单元测试是个好习惯</strong>，我们不能忘。</p>
<p>接下来，我们正式进入 2.0 版本的开发。</p>
<h2 id="2-0：函数式思维"><a href="#2-0：函数式思维" class="headerlink" title="2.0：函数式思维"></a>2.0：函数式思维</h2><p>其实，如果你理解了 1.0 版本的代码，2.0 版本的程序也就不难实现了。因为这个程序的主要功能都已经完成了，现在要做的只是：<strong>换一种思路重构代码</strong>。</p>
<p>我们先来看看 KtHttpV1 这个单例的成员变量：</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">object</span> KtHttpV1 &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">var</span> okHttpClient: OkHttpClient = OkHttpClient()</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">var</span> gson: Gson = Gson()</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="type">&lt;T&gt;</span> <span class="title">create</span><span class="params">(service: <span class="type">Class</span>&lt;<span class="type">T</span>&gt;)</span></span>: T &#123;&#125;</span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="title">invoke</span><span class="params">(url: <span class="type">String</span>, method: <span class="type">Method</span>, args: <span class="type">Array</span>&lt;<span class="type">Any</span>&gt;)</span></span>: Any? &#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>okHttpClient、gson 这两个成员是不支持懒加载的，因此我们首先应该让它们<strong>支持懒加载</strong>。</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">object</span> KtHttpV2 &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">val</span> okHttpClient <span class="keyword">by</span> lazy &#123; OkHttpClient() &#125;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">val</span> gson <span class="keyword">by</span> lazy &#123; Gson() &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="type">&lt;T&gt;</span> <span class="title">create</span><span class="params">(service: <span class="type">Class</span>&lt;<span class="type">T</span>&gt;)</span></span>: T &#123;&#125;</span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="title">invoke</span><span class="params">(url: <span class="type">String</span>, method: <span class="type">Method</span>, args: <span class="type">Array</span>&lt;<span class="type">Any</span>&gt;)</span></span>: Any? &#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这里，我们直接使用了 by lazy 委托的方式，它简洁的语法可以让我们快速实现懒加载。</p>
<p>接下来，我们再来看看 create() 这个方法的定义：</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">//                      注意这里</span></span><br><span class="line"><span class="comment">//                         ↓</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="type">&lt;T&gt;</span> <span class="title">create</span><span class="params">(service: <span class="type">Class</span>&lt;<span class="type">T</span>&gt;)</span></span>: T &#123;</span><br><span class="line">    <span class="keyword">return</span> Proxy.newProxyInstance(</span><br><span class="line">        service.classLoader,</span><br><span class="line">        arrayOf&lt;Class&lt;*&gt;&gt;(service)</span><br><span class="line">    ) &#123; proxy, method, args -&gt;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在上面的代码中，create() 会接收一个Class类型的参数。其实，针对这样的情况，我们完全可以省略掉这个参数。具体做法，是使用我们前面学过的inline，来实现<strong>类型实化</strong>（Reified Type）。我们常说，Java 的泛型是伪泛型，而这里我们要实现的就是真泛型。</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">//  注意这两个关键字</span></span><br><span class="line"><span class="comment">//  ↓          ↓</span></span><br><span class="line"><span class="keyword">inline</span> <span class="function"><span class="keyword">fun</span> <span class="type">&lt;<span class="keyword">reified</span> T&gt;</span> <span class="title">create</span><span class="params">()</span></span>: T &#123;</span><br><span class="line">    <span class="keyword">return</span> Proxy.newProxyInstance(</span><br><span class="line">        T::<span class="keyword">class</span>.java.classLoader, <span class="comment">// ① 变化在这里</span></span><br><span class="line">        arrayOf(T::<span class="keyword">class</span>.java) <span class="comment">// ② 变化在这里</span></span><br><span class="line">    ) &#123; proxy, method, args -&gt;</span><br><span class="line">        <span class="comment">// 待重构</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>正常情况下，泛型参数<a target="_blank" rel="noopener" href="https://zh.wikipedia.org/wiki/%E7%B1%BB%E5%9E%8B%E6%93%A6%E9%99%A4">类型会被擦除</a>，这就是 Java 的泛型被称为“伪泛型”的原因。而通过使用 <strong>inline</strong> 和 <strong>reified</strong> 这两个关键字，我们就能实现类型实化，也就是“真泛型”，进一步，我们就可以在代码注释①、②的地方，使用“T::class.java”来得到 Class 对象。</p>
<p>下面，我们来看看 KtHttp 的主要逻辑该如何重构。</p>
<p>为了方便理解，我们会使用 Kotlin 标准库当中已有的高阶函数，尽量不去涉及函数式编程里的高级概念。在这里我强烈建议你打开 <strong>IDE 一边敲代码一边阅读</strong>，这样一来，当你遇到不熟悉的标准函数时，就可以随时去看它的实现源码了。相信在学习过第 7 讲的高阶函数以后，这些库函数都不会难倒你。</p>
<p>首先，我们来看看 create() 里面“待重构”的代码该如何写。在这个方法当中，我们需要读取 method 当中的 GET 注解，解析出它的值，然后与 baseURL 拼接。这里我们完全可以<strong>借助 Kotlin 的标准库函数</strong>来实现：</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">inline</span> <span class="function"><span class="keyword">fun</span> <span class="type">&lt;<span class="keyword">reified</span> T&gt;</span> <span class="title">create</span><span class="params">()</span></span>: T &#123;</span><br><span class="line">    <span class="keyword">return</span> Proxy.newProxyInstance(</span><br><span class="line">        T::<span class="keyword">class</span>.java.classLoader,</span><br><span class="line">        arrayOf(T::<span class="keyword">class</span>.java)</span><br><span class="line">    ) &#123; proxy, method, args -&gt;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span><span class="symbol">@newProxyInstance</span> method.annotations</span><br><span class="line">            .filterIsInstance&lt;GET&gt;()</span><br><span class="line">            .takeIf &#123; it.size == <span class="number">1</span> &#125;</span><br><span class="line">            ?.let &#123; invoke(<span class="string">&quot;<span class="variable">$baseUrl</span><span class="subst">$&#123;it[<span class="number">0</span>].value&#125;</span>&quot;</span>, method, args) &#125;</span><br><span class="line">    &#125; <span class="keyword">as</span> T</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这段代码的可读性很好，我们可以像读英语文本一样来阅读：</p>
<ul>
<li>首先，我们通过 method.annotations，来获取 method 的所有注解；</li>
<li>接着，我们用filterIsInstance()，来筛选出我们想要找的 GET 注解。这里的 filterIsInstance 其实是 filter 的升级版，也就是过滤的意思；</li>
<li>之后，我们判断 GET 注解的数量，它的数量必须是 1，其他的都不行，这里的 takeIf 其实相当于我们的 if；</li>
<li>最后，我们通过拼接出 URL，然后将程序执行流程交给 invoke() 方法。这里的”?.let{}”相当于判空。</li>
</ul>
<p>好了，create() 方法的重构已经完成，接下来我们来看看 invoke() 方法该如何重构。</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">invoke</span><span class="params">(url: <span class="type">String</span>, method: <span class="type">Method</span>, args: <span class="type">Array</span>&lt;<span class="type">Any</span>&gt;)</span></span>: Any? =</span><br><span class="line">    method.parameterAnnotations</span><br><span class="line">        .takeIf &#123; method.parameterAnnotations.size == args.size &#125;</span><br><span class="line">        ?.mapIndexed &#123; index, it -&gt; Pair(it, args[index]) &#125;</span><br><span class="line">        ?.fold(url, ::parseUrl)</span><br><span class="line">        ?.let &#123; Request.Builder().url(it).build() &#125;</span><br><span class="line">        ?.let &#123; okHttpClient.newCall(it).execute().body?.string() &#125;</span><br><span class="line">        ?.let &#123; gson.fromJson(it, method.genericReturnType) &#125;</span><br></pre></td></tr></table></figure>

<p>这段代码读起来也不难，我们一行一行来分析。</p>
<ul>
<li>第一步，我们通过 method.parameterAnnotations，获取方法当中所有的参数注解，在这里也就是@Field(“lang”)、@Field(“since”)。</li>
<li>第二步，我们通过 takeIf 来判断，参数注解数组的数量与参数的数量相等，也就是说@Field(“lang”)、@Field(“since”)的数量是 2，那么[“Kotlin”, “weekly”]的 size 也应该是 2，它必须是一一对应的关系。</li>
<li>第三步，我们将@Field(“lang”)与”Kotlin”进行配对，将@Field(“since”)与”weekly”进行配对。这里的 mapIndexed，其实就是 map 的升级版，它本质还是一种映射的语法，“注解数组类型”映射成了“Pair 数组”，只是多了一个 index 而已。</li>
<li>第四步，我们使用 fold 与 parseUrl() 这个方法，拼接出完整的 URL，也就是：<a target="_blank" rel="noopener" href="https://baseurl.com/repo?lang=Kotlin&since=weekly%E3%80%82">https://baseurl.com/repo?lang=Kotlin&amp;since=weekly。</a> 这里我们使用了函数引用的语法“::parseUrl”。而 fold 这个操作符，其实就是高阶函数版的 for 循环。</li>
<li>第五步，我们构建出 OkHttp 的 Request 对象，并且将 URL 传入了进去，准备做网络请求。</li>
<li>第六步，我们通过 okHttpClient 发起了网络请求，并且拿到了 String 类型的 JSON 数据。最后，我们通过 Gson 解析出 JSON 的内容，并且返回 RepoList 对象。</li>
</ul>
<p>到目前为止，我们的 invoke() 方法的主要流程就分析完了，接下来我们再来看看用于实现 URL 拼接的 parseUrl() 是如何实现的。</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">parseUrl</span><span class="params">(acc: <span class="type">String</span>, pair: <span class="type">Pair</span>&lt;<span class="type">Array</span>&lt;<span class="type">Annotation</span>&gt;, Any&gt;)</span></span> =</span><br><span class="line">    pair.first.filterIsInstance&lt;Field&gt;()</span><br><span class="line">        .first()</span><br><span class="line">        .let &#123; field -&gt;</span><br><span class="line">            <span class="keyword">if</span> (acc.contains(<span class="string">&quot;?&quot;</span>)) &#123;</span><br><span class="line">                <span class="string">&quot;<span class="variable">$acc</span>&amp;<span class="subst">$&#123;field.value&#125;</span>=<span class="subst">$&#123;pair.second&#125;</span>&quot;</span></span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="string">&quot;<span class="variable">$acc</span>?<span class="subst">$&#123;field.value&#125;</span>=<span class="subst">$&#123;pair.second&#125;</span>&quot;</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br></pre></td></tr></table></figure>

<p>可以看到，这里我们只是把从前的 for 循环代码，换成了 <strong>Kotlin 的集合操作符</strong>而已。大致流程如下：</p>
<ul>
<li>首先，我们从注解的数组里筛选出 Field 类型的注解；</li>
<li>接着，通过 first() 取出第一个 Field 注解，这里它也应该是唯一的；</li>
<li>最后，我们判断当前的 acc 是否已经拼接过参数，如果没有拼接过，就用“?”分隔，如果已经拼接过参数，我们就用“&amp;”分隔。</li>
</ul>
<p>至此，我们 2.0 版本的代码就完成了，完整的代码如下：</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">object</span> KtHttpV2 &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">val</span> okHttpClient <span class="keyword">by</span> lazy &#123; OkHttpClient() &#125;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">val</span> gson <span class="keyword">by</span> lazy &#123; Gson() &#125;</span><br><span class="line">    <span class="keyword">var</span> baseUrl = <span class="string">&quot;https://baseurl.com&quot;</span> <span class="comment">// 可改成任意url</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">inline</span> <span class="function"><span class="keyword">fun</span> <span class="type">&lt;<span class="keyword">reified</span> T&gt;</span> <span class="title">create</span><span class="params">()</span></span>: T &#123;</span><br><span class="line">        <span class="keyword">return</span> Proxy.newProxyInstance(</span><br><span class="line">            T::<span class="keyword">class</span>.java.classLoader,</span><br><span class="line">            arrayOf(T::<span class="keyword">class</span>.java)</span><br><span class="line">        ) &#123; proxy, method, args -&gt;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">return</span><span class="symbol">@newProxyInstance</span> method.annotations</span><br><span class="line">                .filterIsInstance&lt;GET&gt;()</span><br><span class="line">                .takeIf &#123; it.size == <span class="number">1</span> &#125;</span><br><span class="line">                ?.let &#123; invoke(<span class="string">&quot;<span class="variable">$baseUrl</span><span class="subst">$&#123;it[<span class="number">0</span>].value&#125;</span>&quot;</span>, method, args) &#125;</span><br><span class="line">        &#125; <span class="keyword">as</span> T</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="title">invoke</span><span class="params">(url: <span class="type">String</span>, method: <span class="type">Method</span>, args: <span class="type">Array</span>&lt;<span class="type">Any</span>&gt;)</span></span>: Any? =</span><br><span class="line">        method.parameterAnnotations</span><br><span class="line">            .takeIf &#123; method.parameterAnnotations.size == args.size &#125;</span><br><span class="line">            ?.mapIndexed &#123; index, it -&gt; Pair(it, args[index]) &#125;</span><br><span class="line">            ?.fold(url, ::parseUrl)</span><br><span class="line">            ?.let &#123; Request.Builder().url(it).build() &#125;</span><br><span class="line">            ?.let &#123; okHttpClient.newCall(it).execute().body?.string() &#125;</span><br><span class="line">            ?.let &#123; gson.fromJson(it, method.genericReturnType) &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">parseUrl</span><span class="params">(acc: <span class="type">String</span>, pair: <span class="type">Pair</span>&lt;<span class="type">Array</span>&lt;<span class="type">Annotation</span>&gt;, Any&gt;)</span></span> =</span><br><span class="line">        pair.first.filterIsInstance&lt;Field&gt;()</span><br><span class="line">            .first()</span><br><span class="line">            .let &#123; field -&gt;</span><br><span class="line">                <span class="keyword">if</span> (acc.contains(<span class="string">&quot;?&quot;</span>)) &#123;</span><br><span class="line">                    <span class="string">&quot;<span class="variable">$acc</span>&amp;<span class="subst">$&#123;field.value&#125;</span>=<span class="subst">$&#123;pair.second&#125;</span>&quot;</span></span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    <span class="string">&quot;<span class="variable">$acc</span>?<span class="subst">$&#123;field.value&#125;</span>=<span class="subst">$&#123;pair.second&#125;</span>&quot;</span></span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>对应的，我们可以再看看 1.0 版本的完整代码：</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">object</span> KtHttpV1 &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">var</span> okHttpClient: OkHttpClient = OkHttpClient()</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">var</span> gson: Gson = Gson()</span><br><span class="line">    <span class="keyword">var</span> baseUrl = <span class="string">&quot;https://baseurl.com&quot;</span> <span class="comment">// 可改成任意url</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="type">&lt;T&gt;</span> <span class="title">create</span><span class="params">(service: <span class="type">Class</span>&lt;<span class="type">T</span>&gt;)</span></span>: T &#123;</span><br><span class="line">        <span class="keyword">return</span> Proxy.newProxyInstance(</span><br><span class="line">            service.classLoader,</span><br><span class="line">            arrayOf&lt;Class&lt;*&gt;&gt;(service)</span><br><span class="line">        ) &#123; proxy, method, args -&gt;</span><br><span class="line">            <span class="keyword">val</span> annotations = method.annotations</span><br><span class="line">            <span class="keyword">for</span> (<span class="keyword">annotation</span> <span class="keyword">in</span> annotations) &#123;</span><br><span class="line">                <span class="keyword">if</span> (<span class="keyword">annotation</span> <span class="keyword">is</span> GET) &#123;</span><br><span class="line">                    <span class="keyword">val</span> url = baseUrl + <span class="keyword">annotation</span>.value</span><br><span class="line">                    <span class="keyword">return</span><span class="symbol">@newProxyInstance</span> invoke(url, method, args!!)</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span><span class="symbol">@newProxyInstance</span> <span class="literal">null</span></span><br><span class="line"></span><br><span class="line">        &#125; <span class="keyword">as</span> T</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">invoke</span><span class="params">(path: <span class="type">String</span>, method: <span class="type">Method</span>, args: <span class="type">Array</span>&lt;<span class="type">Any</span>&gt;)</span></span>: Any? &#123;</span><br><span class="line">        <span class="keyword">if</span> (method.parameterAnnotations.size != args.size) <span class="keyword">return</span> <span class="literal">null</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">var</span> url = path</span><br><span class="line">        <span class="keyword">val</span> parameterAnnotations = method.parameterAnnotations</span><br><span class="line">        <span class="keyword">for</span> (i <span class="keyword">in</span> parameterAnnotations.indices) &#123;</span><br><span class="line">            <span class="keyword">for</span> (parameterAnnotation <span class="keyword">in</span> parameterAnnotations[i]) &#123;</span><br><span class="line">                <span class="keyword">if</span> (parameterAnnotation <span class="keyword">is</span> Field) &#123;</span><br><span class="line">                    <span class="keyword">val</span> key = parameterAnnotation.value</span><br><span class="line">                    <span class="keyword">val</span> value = args[i].toString()</span><br><span class="line">                    <span class="keyword">if</span> (!url.contains(<span class="string">&quot;?&quot;</span>)) &#123;</span><br><span class="line">                        url += <span class="string">&quot;?<span class="variable">$key</span>=<span class="variable">$value</span>&quot;</span></span><br><span class="line">                    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                        url += <span class="string">&quot;&amp;<span class="variable">$key</span>=<span class="variable">$value</span>&quot;</span></span><br><span class="line">                    &#125;</span><br><span class="line"></span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">val</span> request = Request.Builder()</span><br><span class="line">            .url(url)</span><br><span class="line">            .build()</span><br><span class="line"></span><br><span class="line">        <span class="keyword">val</span> response = okHttpClient.newCall(request).execute()</span><br><span class="line"></span><br><span class="line">        <span class="keyword">val</span> genericReturnType = method.genericReturnType</span><br><span class="line">        <span class="keyword">val</span> body = response.body</span><br><span class="line">        <span class="keyword">val</span> json = body?.string()</span><br><span class="line">        <span class="keyword">val</span> result = gson.fromJson&lt;Any?&gt;(json, genericReturnType)</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> result</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>可见，1.0 版本、2.0 版本，它们之间可以说是天壤之别。</p>
<h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>好了，这节实战就到这里。接下来我们来简单总结一下：</p>
<ul>
<li><p>在 1.0 版本的代码中，我们灵活利用了<strong>动态代理、泛型、注解、反射</strong>这几个技术，实现了 KtHttp 的基础功能。</p>
</li>
<li><p><strong>动态代理</strong>，由于它的底层原理比较复杂，课程当中我是通过 ApiImpl 这个类，来模拟了它动态生成的 Proxy 类。用这种直观的方式来帮助你理解它存在的意义。</p>
</li>
<li><p>泛型方面，我们将其用在了动态代理的 create() 方法上，后面我们还使用了“类型实化”的技术，也就是 inline + reified 关键字。</p>
</li>
<li><p><strong>注解方面</strong>，我们首先自定义了两个注解，分别是 GET、Field。其中，@GET 用于标记接口的方法，它的值是 URL 的 path；@Field 用于标记参数，它的值是参数的 key。</p>
</li>
<li><p><strong>反射方面</strong>，这个技术点，几乎是贯穿于整个代码实现流程的。我们通过反射的自省能力，去分析 repos() 方法，从 GET 注解当中取出了“&#x2F;repo”这个 path，从注解 Field 当中取出了 lang、since，还取出了 repos() 方法的返回值 RepoList，用于 JSON 数据的解析。</p>
</li>
<li><p>在 2.0 版本的代码中，我们几乎删除了之前所有的代码，以<strong>函数式的思维</strong>重写了 KtHttp 的内部逻辑。在这个版本当中，我们大量地使用了 Kotlin 标准库里的高阶函数，进一步提升了代码的可读性。</p>
</li>
</ul>
<p>在前面的加餐课程当中，我们也讨论过 Kotlin 的编程范式问题。<strong>命令式还是函数式，这完全取决于我们开发者自身</strong>。</p>
<p>相比起前面实战课中的单词频率统计程序，这一次我们的函数式范式的代码，实现起来就没有那么得流畅了。原因其实也很简单，Kotlin 提供了强大的集合操作符，这就让 Kotlin 十分擅长“集合操作”的场景，因此词频统计程序，我们不到 10 行代码就解决了。而对于注解、反射相关的场景，函数式的编程范式就没那么擅长了。</p>
<p>在这节课里，我之所以费尽心思地用函数式风格，重构出 KtHttp 2.0 版本，主要还是想让你看到函数式编程在它不那么擅长的领域表现会如何。毕竟，我们在工作中什么问题都可能会遇到。</p>
</article><div class="post-copyright"><div class="post-copyright__author"><span class="post-copyright-meta">文章作者: </span><span class="post-copyright-info"><a href="https://devsu.top">Carl Su</a></span></div><div class="post-copyright__type"><span class="post-copyright-meta">文章链接: </span><span class="post-copyright-info"><a href="https://devsu.top/posts/e45ff726/">https://devsu.top/posts/e45ff726/</a></span></div><div class="post-copyright__notice"><span class="post-copyright-meta">版权声明: </span><span class="post-copyright-info">本博客所有文章除特别声明外，均采用 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">CC BY-NC-SA 4.0</a> 许可协议。转载请注明来自 <a href="https://devsu.top" target="_blank">码行</a>！</span></div></div><div class="tag_share"><div class="post-meta__tag-list"><a class="post-meta__tags" href="/tags/Kotlin/">Kotlin</a></div><div class="post_share"><div class="social-share" data-image="https://static001.geekbang.org/resource/image/98/14/98bb006b524561dae4e80a864ae2c814.jpg" data-sites="facebook,twitter,wechat,weibo,qq"></div><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/butterfly-extsrc/sharejs/dist/css/share.min.css" media="print" onload="this.media='all'"><script src="https://cdn.jsdelivr.net/npm/butterfly-extsrc/sharejs/dist/js/social-share.min.js" defer></script></div></div><nav class="pagination-post" id="pagination"><div class="prev-post pull-left"><a href="/posts/11b33035/" title="Kotlin编程第一课--(基础篇)加餐二  什么是“表达式思维”？"><img class="cover" src="https://s2.loli.net/2023/03/14/9whvakouKqd61gR.jpg" onerror="onerror=null;src='/img/404.jpg'" alt="cover of previous post"><div class="pagination-info"><div class="label">上一篇</div><div class="prev_info">Kotlin编程第一课--(基础篇)加餐二  什么是“表达式思维”？</div></div></a></div><div class="next-post pull-right"><a href="/posts/f3ad1558/" title="Kotlin编程第一课--(基础篇)11  注解与反射：进阶必备技能"><img class="cover" src="https://img2.baidu.com/it/u=923478948,106462584&amp;fm=253&amp;fmt=auto&amp;app=138&amp;f=JPEG?w=500&amp;h=281" onerror="onerror=null;src='/img/404.jpg'" alt="cover of next post"><div class="pagination-info"><div class="label">下一篇</div><div class="next_info">Kotlin编程第一课--(基础篇)11  注解与反射：进阶必备技能</div></div></a></div></nav><div class="relatedPosts"><div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>相关推荐</span></div><div class="relatedPosts-list"><div><a href="/posts/85d82fa8/" title="Kotlin编程第一课--(基础篇)06  扩展：你的能力边界到底在哪里？"><img class="cover" src="https://static001.geekbang.org/resource/image/b6/1c/b65feyy0bdc4764c6564a95912d7851c.jpg" alt="cover"><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2022-05-06</div><div class="title">Kotlin编程第一课--(基础篇)06  扩展：你的能力边界到底在哪里？</div></div></a></div><div><a href="/posts/be9f17d6/" title="Kotlin编程第一课(目录)"><img class="cover" src="https://img2.baidu.com/it/u=3956021957,3313212296&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500" alt="cover"><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2022-07-10</div><div class="title">Kotlin编程第一课(目录)</div></div></a></div><div><a href="/posts/83a7144c/" title="2023-01-17-Kotlin协程 -  结构化并发"><img class="cover" src="https://s2.loli.net/2023/03/14/xEyFgn24zZPXe15.jpg" alt="cover"><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2023-01-17</div><div class="title">2023-01-17-Kotlin协程 -  结构化并发</div></div></a></div><div><a href="/posts/68bf7d20/" title="Kotlin函数"><img class="cover" src="https://img1.baidu.com/it/u=1546227440,2897989905&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500" alt="cover"><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2022-08-05</div><div class="title">Kotlin函数</div></div></a></div><div><a href="/posts/a20c9e06/" title="Kotlin协程"><img class="cover" src="https://s2.loli.net/2023/03/14/3MHPBy1Q4GncRlF.png" alt="cover"><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2023-02-09</div><div class="title">Kotlin协程</div></div></a></div><div><a href="/posts/8c331668/" title="Kotlin内置函数apply、let、run、with、also等区别"><img class="cover" src="https://s2.loli.net/2023/03/14/OpxWcuy2ktMzoQD.jpg" alt="cover"><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2022-08-05</div><div class="title">Kotlin内置函数apply、let、run、with、also等区别</div></div></a></div></div></div><hr/><div id="post-comment"><div class="comment-head"><div class="comment-headline"><i class="fas fa-comments fa-fw"></i><span> 评论</span></div><div id="comment-switch"><span class="first-comment">Valine</span><span class="switch-btn"></span><span class="second-comment">Disqus</span></div></div><div class="comment-wrap"><div><div class="vcomment" id="vcomment"></div></div><div><div id="disqus_thread"></div></div></div></div></div><div class="aside-content" id="aside-content"><div class="sticky_layout"><div class="card-widget" id="card-toc"><div class="item-headline"><i class="fas fa-stream"></i><span>目录</span><span class="toc-percentage"></span></div><div class="toc-content"><ol class="toc"><li class="toc-item toc-level-2"><a class="toc-link" href="#1-0%EF%BC%9AJava-%E6%80%9D%E7%BB%B4"><span class="toc-number">1.</span> <span class="toc-text">1.0：Java 思维</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#2-0%EF%BC%9A%E5%87%BD%E6%95%B0%E5%BC%8F%E6%80%9D%E7%BB%B4"><span class="toc-number">2.</span> <span class="toc-text">2.0：函数式思维</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%B0%8F%E7%BB%93"><span class="toc-number">3.</span> <span class="toc-text">小结</span></a></li></ol></div></div></div></div></main><footer id="footer"><div id="footer-wrap"><div class="copyright">&copy;2018 - 2023 By Carl Su</div><div class="framework-info"><span>框架 </span><a target="_blank" rel="noopener" href="https://hexo.io">Hexo</a><span class="footer-separator">|</span><span>主题 </span><a target="_blank" rel="noopener" href="https://github.com/jerryc127/hexo-theme-butterfly">Butterfly</a></div><div class="footer_custom_text">学无止境</div></div></footer></div><div id="rightside"><div id="rightside-config-hide"><button id="readmode" type="button" title="阅读模式"><i class="fas fa-book-open"></i></button><button id="translateLink" type="button" title="简繁转换">繁</button><button id="darkmode" type="button" title="浅色和深色模式转换"><i class="fas fa-adjust"></i></button><button id="hide-aside-btn" type="button" title="单栏和双栏切换"><i class="fas fa-arrows-alt-h"></i></button></div><div id="rightside-config-show"><button id="rightside_config" type="button" title="设置"><i class="fas fa-cog fa-spin"></i></button><button class="close" id="mobile-toc-button" type="button" title="目录"><i class="fas fa-list-ul"></i></button><button id="chat_btn" type="button" title="聊天"><i class="fas fa-sms"></i></button><a id="to_comment" href="#post-comment" title="直达评论"><i class="fas fa-comments"></i></a><button id="go-up" type="button" title="回到顶部"><span class="scroll-percent"></span><i class="fas fa-arrow-up"></i></button></div></div><div><script src="/js/utils.js"></script><script src="/js/main.js"></script><script src="/js/tw_cn.js"></script><script src="https://cdn.jsdelivr.net/npm/@fancyapps/ui/dist/fancybox.umd.min.js"></script><script src="https://cdn.jsdelivr.net/npm/vanilla-lazyload/dist/lazyload.iife.min.js"></script><script src="https://cdn.jsdelivr.net/npm/node-snackbar/dist/snackbar.min.js"></script><div class="js-pjax"><script>function loadValine () {
  function initValine () {
    const valine = new Valine(Object.assign({
      el: '#vcomment',
      appId: '',
      appKey: '',
      avatar: 'monsterid',
      serverURLs: '',
      emojiMaps: "",
      path: window.location.pathname,
      visitor: false
    }, null))
  }

  if (typeof Valine === 'function') initValine() 
  else getScript('https://cdn.jsdelivr.net/npm/valine/dist/Valine.min.js').then(initValine)
}

if ('Valine' === 'Valine' || !true) {
  if (true) btf.loadComment(document.getElementById('vcomment'),loadValine)
  else setTimeout(loadValine, 0)
} else {
  function loadOtherComment () {
    loadValine()
  }
}</script><script>function loadDisqus () {
  var disqus_config = function () {
    this.page.url = 'https://devsu.top/posts/e45ff726/'
    this.page.identifier = '/posts/e45ff726/'
    this.page.title = 'Kotlin编程第一课--(基础篇)12  实战：用Kotlin实现一个网络请求框架KtHttp'
  };

  window.disqusReset = () => {
    DISQUS.reset({
      reload: true,
      config: disqus_config
    })
  }

  if (window.DISQUS) disqusReset()
  else {
    (function() { 
      var d = document, s = d.createElement('script');
      s.src = 'https://.disqus.com/embed.js';
      s.setAttribute('data-timestamp', +new Date());
      (d.head || d.body).appendChild(s);
    })();
  }

  document.getElementById('darkmode').addEventListener('click', () => {
    setTimeout(() => window.disqusReset(), 200)
  })
}

if ('Valine' === 'Disqus' || !true) {
  if (true) btf.loadComment(document.getElementById('disqus_thread'), loadDisqus)
  else loadDisqus()
} else {
  function loadOtherComment () {
    loadDisqus()
  }
}
</script></div><script defer="defer" id="fluttering_ribbon" mobile="false" src="https://cdn.jsdelivr.net/npm/butterfly-extsrc/dist/canvas-fluttering-ribbon.min.js"></script><script src="https://cdn.jsdelivr.net/npm/butterfly-extsrc/dist/activate-power-mode.min.js"></script><script>POWERMODE.colorful = true;
POWERMODE.shake = false;
POWERMODE.mobile = false;
document.body.addEventListener('input', POWERMODE);
</script><script id="click-heart" src="https://cdn.jsdelivr.net/npm/butterfly-extsrc/dist/click-heart.min.js" async="async" mobile="false"></script><script async data-pjax src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script></div><div id="local-search"><div class="search-dialog"><nav class="search-nav"><span class="search-dialog-title">搜索</span><span id="loading-status"></span><button class="search-close-button"><i class="fas fa-times"></i></button></nav><div class="is-center" id="loading-database"><i class="fas fa-spinner fa-pulse"></i><span>  数据库加载中</span></div><div class="search-wrap"><div id="local-search-input"><div class="local-search-box"><input class="local-search-box--input" placeholder="搜索文章" type="text"/></div></div><hr/><div id="local-search-results"></div></div></div><div id="search-mask"></div><script src="/js/search/local-search.js"></script></div></body></html>